from twisted.internet.protocol import Factory
from twisted.protocols import amp
from twisted.internet.endpoints import TCP4ServerEndpoint
from twisted.internet import reactor
from snakecommon import *
import random, math
import config

DEBUG_ON = True

def DEBUG_PRINT(str):
    if DEBUG_ON:
        print str

HEAD_START_POS = [(6,7),(18,7)]
FOOD_START      = (12,3)
INIT_HEADING    = Heading.DOWN

class SnakePlayer(amp.AMP):
    class SnakePlayerState:
        HANDSHAKING   = 0
        CONNECTED     = 1
        DISCONNECTED  = 2
    HANDSHAKE_WAIT_PERIOD = 2.0 ## seconds, time until connection is booted

    def __init__(self):
        super(SnakePlayer,self).__init__()
        self.connection_state      = SnakePlayer.SnakePlayerState.HANDSHAKING
        self.session               = None
        self.factory               = None ## Set in the Factory method
        self.boot_for_no_handshake = None
        ## The reason for this is that the unit-tests may install a different
        ## reactor for testing purposes.
        self.reactor               = reactor

    def getIP4(self):
        try:
            return self.transport.getPeer().host
        except:
            return "No IP"

    def connectionMade(self):
        DEBUG_PRINT("New connection from %s"%self.getIP4())

        self.boot_for_no_handshake = self.reactor.callLater(
            SnakePlayer.HANDSHAKE_WAIT_PERIOD,
            self.kickPlayer,
            "Failed to handshake in time")

    def connectionLost(self,reason):
        DEBUG_PRINT("Lost connection from %s"%self.getIP4())
        self.connection_state = SnakePlayer.SnakePlayerState.DISCONNECTED

        if self.boot_for_no_handshake and self.boot_for_no_handshake.active():
            self.boot_for_no_handshake.cancel()
        if self.session:
            self.session.connectionLost(reason)

    def kickPlayer(self,reason):
        DEBUG_PRINT("Connection from %s was kicked for: %s"%(self.getIP4(),reason))

        if self.boot_for_no_handshake and self.boot_for_no_handshake.active():
            self.boot_for_no_handshake.cancel()

        self.callRemote(Kick,reason=reason)
        self.transport.loseConnection()

    def remoteHandshake(self):
        if not self.connection_state == SnakePlayer.SnakePlayerState.HANDSHAKING:
            self.kickPlayer("Protocol error (can't handshake)")

        self.connection_state   = SnakePlayer.SnakePlayerState.CONNECTED
        self.boot_for_no_handshake.cancel()
        self.boot_for_no_handshake = None

        self.session = self.factory.sessionmanager.requestNewSession(self)

        DEBUG_PRINT("Connection from %s has identified. New ID: %s"%(self.getIP4(),self.session.id))
        return {'session_id':self.session.id}

    Handshake.responder(remoteHandshake)

    def remoteLobbyStateChange(self,id,lobby_state):
        if not self.connection_state == SnakePlayer.SnakePlayerState.CONNECTED:
            self.kickPlayer("Protocol error (can't change lobby state)")
        self.session.clientChangedLobbyState(lobby_state)
        return {}
    LobbyStateChange.responder(remoteLobbyStateChange)

    def remoteSetHeading(self,id,new_heading):
        # id parameter is not used since the session id is known
        if not self.connection_state == SnakePlayer.SnakePlayerState.CONNECTED:
            self.kickPlayer("Protocol error (can't change heading)")
        self.session.clientChangedHeading(new_heading)
        return {}
    SetHeading.responder(remoteSetHeading)

class Session:
    def __init__(self):
        #self.state          = LobbyState.NOT_READY
        self.state = LobbyState.READY
        self.protocol       = None # Set by SessionManager
        self.sessionmanager = None # Set by SessionManager
        self.id             = None # Set by SessionManager

    def __repr__(self):
        return "Session. ID: %s, IP: %s"%(self.id,self.protocol.getIP4())

## Connection events
    def connectionLost(self,reason): self.sessionmanager.removeSession(self)

## Server level events
    def endSession(self,reason): self.protocol.kickPlayer(reason)

## Client->Server functions
    def clientChangedLobbyState(self,new_state):
        print "[From:%s] Remote changed lobby state to %s"%(self.id,new_state)
        self.sessionmanager.playerChangedLobbyState(self,new_state)

    def clientChangedHeading(self,new_heading):
        print "[From:%s] Changed heading to %s"%(self.id,new_heading)
        self.sessionmanager.playerChangedHeading(self,new_heading)

## Server -> Client functions (lobby)
    def sendAddSession(self,session_id,lobby_state):
        print "[To:%s] Sending session add (id: %s)"%(self.id,session_id)
        self.protocol.callRemote(AddSession,id=session_id,lobby_state=lobby_state)

    def sendRemoveSession(self,session_id):
        print "[To:%s] Sending remove session (id:%s)"%(self.id,session_id)
        self.protocol.callRemote(RemoveSession,id=session_id)

    def sendLobbyState(self,id,state):
        print "[To:%s] Sending %s's lobby state"%(self.id,id)
        self.protocol.callRemote(LobbyStateChange,id=id,lobby_state=state)

    def startGame(self):
        print "[To:%s] Sending game start"%self.id
        self.protocol.callRemote(ChangeProgramState,
            program_state=ProgramState.GAME)

    def endGame(self):
        print "[To:%s] Sending game end"%self.id
        self.protocol.callRemote(ChangeProgramState,
            program_state=ProgramState.LOBBY)

    def setHeading(self,id,heading):
        print "[To:%s] Sending opponent heading (%s)"%(self.id,heading)
        self.protocol.callRemote(SetHeading,
            id=id,new_heading=heading)

    def addSnakePart(self,id,pos):
        print "[To:%s] Adding %s to snake %s"%(self.id,pos,id)
        self.protocol.callRemote(AddSnakePart,
            id=id,row=pos[0],col=pos[1])

    def placeFood(self,pos):
        print "[To:%s] Setting position of food to (%s)"%(self.id,pos)
        self.protocol.callRemote(PlaceFood,row=pos[0],col=pos[1])

    def tick(self,id,pos):
        print "[To:%s] Ticking snake %s"%(self.id,id)
        self.protocol.callRemote(Tick,id=id,row=pos[0],col=pos[1])

## Server -> Client functions (in-game)

class SessionManager:
    def __init__(self):
        self.game            = None
        self.program_state   = ProgramState.LOBBY
        self.sessions        = []
        self.next_session_id = 0

    def requestNewSession(self,protocol):
        s = Session()
        s.protocol       = protocol
        s.sessionmanager = self
        s.id             = self.next_session_id
        self.next_session_id += 1

        if len(self.sessions)<2:
            self.onNewSession(s)
        else:
            s.endSession("Server is full")
        return s

    def playerChangedLobbyState(self,session,new_state):
        for s_iter in self.sessions:
            if s_iter == session:
                s_iter.state = new_state
            else:
                s_iter.sendLobbyState(session.id,new_state)

        if ((len(self.sessions) == 2) and
            all([s.state == LobbyState.READY for s in self.sessions])):
            self.startNewGame()

    def playerChangedHeading(self,session,new_heading):
        ## Protocol breach...
        if not self.program_state == ProgramState.GAME:
            self.removeSession(session)
        else:
            self.game.remoteSetHeading(session,new_heading)
            for s_iter in self.sessions:
                s_iter.setHeading(session.id,new_heading)

    def onNewSession(self,new_session):
        for session in self.sessions:
            session.sendAddSession(new_session.id,new_session.state)
            new_session.sendAddSession(session.id,session.state)

        self.sessions.append(new_session)
        # Send the new session info about itself..
        new_session.sendAddSession(new_session.id,new_session.state)

    def removeSession(self,session):
        if session in self.sessions:
            self.sessions.remove(session)

        if not self.program_state == ProgramState.LOBBY:
            self.endGame()
            for s_iter in self.sessions:
                s_iter.endGame()

        for s_iter in self.sessions:
            s_iter.sendRemoveSession(session.id)

    def endGame(self):
        self.program_state = ProgramState.LOBBY
        self.game.end()

    def startNewGame(self):
        for session in self.sessions:
            session.state = LobbyState.NOT_READY
            for s_iter in self.sessions:
                s_iter.sendLobbyState(session.id,session.state)

        self.program_state = ProgramState.GAME
        self.game = Game(self)
        self.game.start()

    def kickAll(self):
        for session in self.sessions:
            session.endSession("Shutting down")

class Snake:
    def __init__(self):
        self.parts   = []
        self.heading = Heading.DOWN

class Game:
    def __init__(self,session_mgr):
        self.session_mgr = session_mgr
        self.food_count  = 1
        self.food        = FOOD_START
        self.update_call = None

        self.snakes      = {}
        for session in self.session_mgr.sessions:
            self.snakes[session] = Snake()

    def remoteSetHeading(self,session,heading):
        self.snakes[session].heading = heading

    def scheduleUpdate(self):
        dt = 100 + 400*math.exp(-0.05*self.food_count)
        self.update_call = reactor.callLater(dt/1000,self.update)

    def start(self):
        sessions = self.snakes.keys()

        for session in sessions:
            session.startGame()

        for index, tup in enumerate(self.snakes.iteritems()):
            session = tup[0]
            snake   = tup[1]
            start_pos = HEAD_START_POS[index]
            for s_iter in sessions:
                s_iter.setHeading(session.id,snake.heading)
            self.addPart(session,start_pos)

        self.placeFood()
        self.scheduleUpdate()

    def addPart(self,session,pos):
        # Sends to both clients
        for s_iter, snake in self.snakes.iteritems():
            s_iter.addSnakePart(session.id,pos)
        self.snakes[session].parts.insert(0,pos)

    def placeFood(self):
        if self.food_count > 1:
            occupied_pos = []
            for snake in self.snakes.values():
                occupied_pos += snake.parts

            self.food = (random.randrange(0,H_TILES),random.randrange(0,V_TILES))
            while self.food in occupied_pos:
                self.food = (random.randrange(0,H_TILES),random.randrange(0,V_TILES))
        else:
            self.food = FOOD_START

        for session in self.snakes.keys():
            session.placeFood(self.food)
        self.food_count += 1

    def checkCollisions(self):
        for snake in self.snakes.values():
            head_x = snake.parts[0][0]
            head_y = snake.parts[0][1]

            other_parts = snake.parts[1:]
            for snake_it in self.snakes.values():
                if not snake == snake_it:
                    other_parts += snake_it.parts

            if ( (head_x < 0) or (head_x >= H_TILES) or
                 (head_y < 0) or (head_y >= V_TILES) or
                 (snake.parts[0] in other_parts)   ):
                     self.session_mgr.endGame()

    def end(self):
        if self.update_call.active():
            self.update_call.cancel()

        for session in self.snakes.keys():
            session.endGame()
        # cancel the next update call

    def updateSnakes(self):
        heading_delta    = {Heading.UP:(0,-1), Heading.RIGHT:(1,0),
                            Heading.DOWN:(0,1),Heading.LEFT:(-1,0)}

        for session, snake in self.snakes.iteritems():
            dx = heading_delta[snake.heading][0]
            dy = heading_delta[snake.heading][1]

            next_coordinate = ( snake.parts[0][0] + dx,
                                snake.parts[0][1] + dy)
            if next_coordinate == self.food:
                self.addPart(session,next_coordinate)
                self.placeFood()
            else:
                snake.parts.insert(0,next_coordinate)
                snake.parts.pop(-1)

                for session_it in self.snakes.keys():
                    session_it.tick(session.id,next_coordinate)

    def update(self):
        self.updateSnakes()
        self.checkCollisions()
        if self.session_mgr.program_state == ProgramState.GAME:
            self.scheduleUpdate()

class SnakePlayerFactory(Factory):
    def __init__(self):
        self.players = []
        self.sessionmanager = SessionManager()

    def buildProtocol(self,addr):
        s =  SnakePlayer()
        s.factory = self
        return s

if __name__ == '__main__':
    endpoint = TCP4ServerEndpoint(reactor,config.port)
    endpoint.listen(SnakePlayerFactory())
    reactor.run()